package com.hxzy.controller;

import com.hxzy.common.enums.AckCode;
import com.hxzy.common.exception.ServiceStringException;
import com.hxzy.common.resp.R;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Author wy
 * @Description 连续签到功能
 * @Date 2023/5/19 14:02
 * @Version 1.0
 */
@Log4j2
@RestController
@RequestMapping(value = "/redis/bitmap")
public class RedisBitMapController {

    @Autowired
    private RedisTemplate redisTemplate;


    @GetMapping(value = "/sign/{userId}")
    public R executeSign(@PathVariable(value = "userId") Integer userId){
        if(checkSign(userId,LocalDate.now())){
           throw new ServiceStringException("今天已签到过,不需要再到过了");
        }
        boolean sign = sign(userId);
        return R.okHasData(sign);
    }

    /**
     * 检查某天是否打卡
     * @param userId
     * @param date
     * @return
     */
    @GetMapping(value = "/checksign/{userId}/{date}")
    public R checkSignValue(@PathVariable(value = "userId") Integer userId,@PathVariable(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date){
        boolean sign = this.checkSign(userId, date);
        return R.okHasData(sign?"打卡了":"未打卡");
    }


    /**
     * 检查某个月连续打卡时间
     * @param userId
     * @param date
     * @return
     */
    @GetMapping(value = "/signCount/{userId}/{date}")
    public R signCount(@PathVariable(value = "userId") Integer userId,@PathVariable(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date){
        int count=getSignCount(userId,date);
        return R.okHasData(count);
    }

    /**
     * 检查某个月每天打卡明细
     * @param userId
     * @param date
     * @return
     */
    @GetMapping(value = "/signDetail/{userId}/{date}")
    public R signDetailMap(@PathVariable(value = "userId") Integer userId,@PathVariable(value = "date") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date){
        Map<String, Integer> signDetailMap = getSignDetailMap(userId,0, date);
        return R.okHasData(signDetailMap);
    }

    /**
     * 检查某个月指定日期打卡时间
     * @param userId
     * @param begin
     * @param end
     * @return
     */
    @GetMapping(value = "/signRangeDetail/{userId}/{begin}/{end}")
    public R signRangeDetail(@PathVariable(value = "userId") Integer userId,
                                  @PathVariable(value = "begin") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
                                  @PathVariable(value = "end") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        if(begin.getMonthValue()!=end.getMonthValue() &&  begin.getYear()!=end.getYear()){
           throw new ServiceStringException("只能统计同一年同一月的数据");
        }

        int offsetStart=begin.getDayOfMonth()-1;
        Map<String, Integer> signDetailMap = getSignDetailMap(userId,offsetStart, end);
        return R.okHasData(signDetailMap);
    }

    /**
     * 当前月第一次签到的时间
     * @param userId
     * @param begin
     * @return
     */
    @GetMapping(value = "/firstSign/{userId}/{begin}")
    public R firstSign(@PathVariable(value = "userId") Integer userId,
                                  @PathVariable(value = "begin") @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin){

        LocalDate localDate=this.getFirstSignDate(userId,begin);
        return localDate==null ? R.build(AckCode.FAIL): R.okHasData("首次签到:"+ localDate.toString());
    }

    /**
     * 获取当月首次签到日期
     *
     * @param uid  用户ID
     * @param date 日期
     * @return 首次签到日期
     */
    public LocalDate getFirstSignDate(int uid, LocalDate date) {
        long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(buildSignKey(uid, date).getBytes(), true));
        return bitPosition < 0 ? null : date.withDayOfMonth((int) bitPosition + 1);
    }

    /**
     * 获取连续签到的次数(如果不连续,返回0)
     * @return
     */
    private int getSignCount(Integer userId,LocalDate now) {

        // 组合成key= sign:userId:年月
        String key = buildSignKey(userId, now);
        // 获取当前日期是当月的第多少天
        int dayOfMonth = now.getDayOfMonth();

        // 获取本月截止今天为止的所有签到记录，返回的是一个十进制的数字
        List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));

        log.info("查询数据:{}", result);
        if (result == null || result.isEmpty()) {
            return 0;
        }

        Long number = result.get(0);
        //计算次数
        int count = 0;

        if (number == null || number == 0) {
            return 0;
        }

        while (true) {

            // 让这个数字与1做与运算，得到数字的最后一位bit，并判断这个bit是否为0
            if ((number & 1) == 0) {
                // 如果为0说明不是连续签到，直接终止
                break;
            } else {
                // 如果不为0，那就为1，说明有签到，继续下次循环，并且计数器加一
                count++;
            }
            // 把数字右移一位，抛弃最后一位的bit，继续下一个bit
            number = number >>> 1; // 也可以写成 number >>>= 1
        }
        return count;
    }



    /**
     * 获取指定日期 是否打过卡
     * @return
     */
    private Map<String,Integer> getSignDetailMap(Integer userId,int offsetStart, LocalDate end) {

        //String=天数,0没有,1打了
        Map<String,Integer>  map=new HashMap<>();

        // 组合成key= sign:userId:年月
        String key = buildSignKey(userId, end);
        // 获取当前日期是当月的第多少天
        int dayOfMonth = end.getDayOfMonth();
        int month=end.getMonthValue();

        // 获取本月截止今天为止的所有签到记录，返回的是一个十进制的数字
        List<Long> result = redisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));

        log.info("查询数据:{}", result);
        if (result == null || result.isEmpty()) {
            return map;
        }

        Long number = result.get(0);
        //计算次数
        int count = 0;

        if (number == null || number == 0) {
            return map;
        }


        int temp=dayOfMonth;
        while (temp>0) {

            // 让这个数字与1做与运算，得到数字的最后一位bit，并判断这个bit是否为0
            if ((number & 1) == 0) {
                // 如果为0说明不是连续签到，直接终止
                 map.put( month+"/"+temp, 0);
            } else {
                // 如果不为0，那就为1，说明有签到，继续下次循环，并且计数器加一
                map.put( month+"/"+temp, 1);
            }
            // 把数字右移一位，抛弃最后一位的bit，继续下一个bit
            number = number >>> 1; // 也可以写成 number >>>= 1
            temp--;
        }

        //删除掉不需要的数据
        if(offsetStart>0){
            for(int i=0;i<=offsetStart;i++){
               String removeKey= month+"/"+i;
               map.remove(removeKey);
            }
        }
        return map;
    }

    /**
     * 打卡
     * @param userId
     * @return
     */
    private  boolean  sign( Integer userId) {
        // 组合成key= sign:userId:年月
        String key = buildSignKey(userId,  LocalDate.now());
        // 获取当前日期是当月的第多少天
        LocalDate now = LocalDate.now();
        int dayOfMonth = now.getDayOfMonth();
        // 存到redis中的bitmap中，由于bitmap从0开始，第多少天从1开始，dayOfMonth需要减1
        redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
       return true;
    }




    /**
     * 查询当天是否有签到
     *
     * @param userId  用户id
     * @param localDate localDate 对象
     * @return
     */
    private boolean checkSign(int userId, LocalDate localDate) {
        String key = buildSignKey(userId,  localDate);

        return redisTemplate.opsForValue().getBit(key, localDate.getDayOfMonth() - 1);
    }



    /**
     * 拼接rediskey
     * @param uid  用户id
     * @param date  日期对象
     * @return
     */
    private  String buildSignKey(int uid, LocalDate date) {
        return String.format("sign:%s:%s", uid, formatDate(date));
    }

    /**
     * 日期格式化
     * @param date
     * @return
     */
    private  String formatDate(LocalDate date) {
        return  date.format(DateTimeFormatter.ofPattern("yyyyMM"));
    }





}
